package net.sf.fmj.ejmf.toolkit.media;

import java.awt.Component;
import java.io.IOException;
import java.util.Vector;

import javax.media.Clock;
import javax.media.ClockStartedError;
import javax.media.ClockStoppedException;
import javax.media.Controller;
import javax.media.ControllerErrorEvent;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.DurationUpdateEvent;
import javax.media.GainControl;
import javax.media.IncompatibleSourceException;
import javax.media.IncompatibleTimeBaseException;
import javax.media.NotRealizedError;
import javax.media.Player;
import javax.media.ResourceUnavailableEvent;
import javax.media.Time;
import javax.media.TransitionEvent;
import javax.media.protocol.DataSource;

import net.sf.fmj.ejmf.toolkit.media.event.ManagedControllerErrorEvent;


/**
 * The AbstractPlayer class provides a basic implementation of a
 * javax.media.Player.  The abstract "do" methods from
 * AbstractController have been implemented here to provide support
 * for management of multiple Controllers.  Subclasses of this
 * class should instead implement the following abstract "do"
 * methods to transition their Controller:
 * <p>
 * <UL>
 * <LI>doPlayerRealize()</LI>
 * <LI>doPlayerPrefetch()</LI>
 * <LI>doPlayerSyncStart()</LI>
 * <LI>doPlayerDeallocate()</LI>
 * <LI>doPlayerStop()</LI>
 * </UL>
 * <p>
 * The rules for implementation of these methods are identical to
 * those for implementing a AbstractController:
 * <p>
 * <OL>
 * <LI>Do not return until the state change is complete.  Once the
 *    state change is complete, return ASAP.</LI>
 * <p>
 * <LI>Do not call one another.  They will be called in the correct
 *    order at the correct time.</LI>
 * <p>
 * <LI>Do not set the current or target states.  They are set
 *    automatically.</LI>
 * <p>
 * <LI>Do not post any TransitionEvents.  They are posted
 *    automatically.</LI>
 * <p>
 * <LI>Do not call any of the Clock routines.  They will be called
 *    automatically.</LI>
 * <p>
 * <LI>Return true if successful.  If unsuccessful, post an
 *    appropriate ControllerErrorEvent and return false.</LI>
 * <p>
 * <LI>When the end of the media has been reached, call endOfMedia().
 *    This will post an EndOfMediaEvent and set the appropriate
 *    states.  Do not post an EndOfMediaEvent in any other way.</LI>
 * </OL>
 * <p>
 * Other abstact methods that should be implemented are:
 * <p>
 * <UL>
 * <LI>doPlayerClose()</LI>
 * <LI>doPlayerSetMediaTime()</LI>
 * <LI>doPlayerSetRate()</LI>
 * <LI>getPlayerStartLatency()</LI>
 * <LI>getPlayerDuration()</LI>
 * </UL>
 *
 * <p>
 * From the book: Essential JMF, Gordon, Talley (ISBN 0130801046).  Used with permission.
 * </p>
 * @author     Steve Talley
 */
public abstract class AbstractPlayer extends AbstractController
    implements Player, ControllerListener
{
    private DataSource           source;
    private Vector<Controller>   controllers;
    private Time                 duration;
    private ControllerErrorEvent controllerError;
    private GainControl          gainControl;
    private Component            visualComponent;
    private Component            controlPanelComponent;

    /**
     * Construct a AbstractPlayer.
     */
    public AbstractPlayer() {
        super();

        controllers = new Vector<Controller>();
        duration = new Time(0);
    }

    ////////////////////////////////////////////////////////////
    //
    //  Abstract methods
    //
    ////////////////////////////////////////////////////////////

    /**
     * Implement to realize the Player.
     * <p>
     * This method should not be called directly.  Instead, call
     * realize().
     *
     * @return     True if successful, false otherwise.
     */
    public abstract boolean doPlayerRealize();

    /**
     * Implement to prefetch the Player.
     * <p>
     * This method should not be called directly.  Instead, call
     * prefetch().
     *
     * @return     True if successful, false otherwise.
     */
    public abstract boolean doPlayerPrefetch();

    /**
     * Implement to start the Player.
     * <p>
     * This method should not be called directly.  Instead, call
     * start().
     *
     * @return     True if successful, false otherwise.
     */
    public abstract boolean doPlayerSyncStart(Time t);

    /**
     * Implement to deallocate the Player.
     * <p>
     * This method should not be called directly.  Instead, call
     * deallocate().
     *
     * @return     True if successful, false otherwise.
     */
    public abstract boolean doPlayerDeallocate();

    /**
     * Implement to stop the Player.
     * <p>
     * This method should not be called directly.  Instead, call
     * stop().
     *
     * @return     True if successful, false otherwise.
     */
    public abstract boolean doPlayerStop();

    /**
     * Close the Player.  Typically this method will release as
     * many resources as possible, especially those that may be
     * needed by other Players.
     * <p>
     * This method should not be called directly.  Instead, call
     * close().
     */
    public abstract void doPlayerClose();

    /**
     * Override to provide implementation-specific functionality.
     * When this method is called, it is guaranteed that the
     * Controller is Stopped and that the given time is within the
     * Controller's duration.
     * <p>
     * This method should not be called directly.  Instead, call
     * setMediaTime().
     *
     * @param      t
     *             The media time to set
     */
    public abstract void doPlayerSetMediaTime(Time t);

    /**
     * Override to provide implementation-specific functionality.
     * When this method is called, it is guaranteed that the
     * Controller is Stopped.
     * <p>
     * This method should not be called directly.  Instead, call
     * setRate().
     *
     * @param      rate
     *             The requested rate to set
     *
     * @return     The actual rate that was set
     */
    public abstract float doPlayerSetRate(float rate);

    /**
     * Returns the start latency of the media played by <U>this</U>
     * Player only.  It does not consider any of the Controllers
     * that this Player may be managing.
     */
    public abstract Time getPlayerStartLatency();

    /**
     * Returns the duration of the media played by <U>this</U> Player
     * only.  It does not consider any of the Controllers that
     * this Player may be managing.
     */
    public abstract Time getPlayerDuration();


    ////////////////////////////////////////////////////////////
    //
    //  javax.media.Duration methods
    //
    ////////////////////////////////////////////////////////////

    /**
     * Gets the duration of this Player.
     */
    public synchronized final Time getDuration() {
        if( duration == null ) {
            updateDuration();
        }
        return duration;
    }

    /**
     * Update the duration of this Player.  It is defined to be
     * the longest duration between this Player and any of the
     * Controllers that this Player may be managing.  If any of
     * the Controllers returns DURATION_UNKNOWN or
     * DURATION_UNBOUNDED, then the duration is set to this value.
     */
    private synchronized final void updateDuration() {
        Time duration = getPlayerDuration();

        if( duration != DURATION_UNKNOWN )
        {
            for(int i = 0, n = controllers.size(); i < n; i++) {
                Controller c =
                    (Controller)controllers.elementAt(i);

                Time d = c.getDuration();

                if( d == DURATION_UNKNOWN ) {
                    duration = d;
                    break;
                }

                if( duration != DURATION_UNBOUNDED &&
                    ( d == DURATION_UNBOUNDED ||
                      d.getNanoseconds() >
                      duration.getNanoseconds() ) )
                {
                    duration = d;
                }
            }
        }

        boolean newDuration = false;

        if( duration      == DURATION_UNKNOWN   ||
            duration      == DURATION_UNBOUNDED ||
            this.duration == DURATION_UNKNOWN   ||
            this.duration == DURATION_UNBOUNDED)
        {
            if( this.duration != duration ) {
                newDuration = true;
            }
        } else

        if( this.duration == null ||
        	duration.getNanoseconds() !=
            this.duration.getNanoseconds() )
        {
            newDuration = true;
        }

        //  If the duration has changed since it was last
        //  calculated, update it and post a DurationUpdateEvent

        if( newDuration ) {
            this.duration = duration;
            postEvent( new DurationUpdateEvent(this,duration) );
        }
    }


    ////////////////////////////////////////////////////////////
    //
    //  javax.media.Clock methods
    //
    ////////////////////////////////////////////////////////////

    /**
     * Sets the media time.  If the Player is in the Started
     * state, it is stopped before setting the media time and
     * restarted afterwards.
     *
     * @param      t
     *             The media time to set
     *
     * @exception  NotRealizedError
     *             If the Controller is not Realized.
     */
    public synchronized void setMediaTime(Time t) {
        boolean isStarted = (getState() == Started);
        
        if(isStarted) {
            stopInRestart();
        }

        super.setMediaTime(t);
        
        if(isStarted) {
            start();
        }
    }

    /**
     * Sets the rate.  If the Player is in the Started state, it
     * is stopped before setting the rate and restarted
     * afterwards.
     *
     * @param      rate
     *             The temporal scale factor (rate) to set.
     *
     * @exception  NotRealizedError
     *             If the Controller is not Realized.
     */
    public synchronized float setRate(float rate) {
        boolean isStarted = (getState() == Started);
        
        if(isStarted) {
            stopInRestart();
        }

        float newRate = super.setRate(rate);
        
        if(isStarted) {
            start();
        }

        return newRate;
    }

    ////////////////////////////////////////////////////////////
    //
    //  javax.media.Controller methods
    //
    ////////////////////////////////////////////////////////////

    /**
     * Gets the start latency of this Player.  It is defined to
     * be the longest start latency between this Player and any
     * of the Controllers that this Player may be managing.  If
     * any of the Controllers returns LATENCY_UNKNOWN, it's value
     * is skipped in the calculation of the maximum latency.
     *
     * @return     The maximum start latency of this Player and
     *             all its managed Controllers, or
     *             LATENCY_UNKNOWN if the Player and its managed
     *             Controllers all return LATENCY_UNKNOWN.
     */
    public synchronized Time getStartLatency() {
        int currentState = getState();

        if(currentState == Unrealized ||
           currentState == Realizing)
        {
            throw new NotRealizedError(
                "Cannot get start latency from an Unrealized Controller");
        }

        Time latency = getPlayerStartLatency();

        for(int i = 0, n = controllers.size(); i < n; i++) {
            Controller c = (Controller)controllers.elementAt(i);
            Time l = c.getStartLatency();

            if( l == LATENCY_UNKNOWN ) {
                continue;
            }

            if( latency == LATENCY_UNKNOWN ||
                l.getNanoseconds() > latency.getNanoseconds() )
            {
                latency = l;
            }
        }

        return latency;
    }

    ////////////////////////////////////////////////////////////
    //
    //  javax.media.Player methods
    //
    ////////////////////////////////////////////////////////////

    /**
     * Start the player on a new thread.  If necessary, the
     * Player will be prefetched before being started.
     * Subclasses should override doSyncStart() to do the actual work
     * to transition the Controller.
     * <p>
     * Checks for Player state prerequisites and creates a
     * StartThread to start the AbstractPlayer.  If there
     * is already a thread transitioning the AbstractPlayer
     * forward, then the target state of the AbstractPlayer is
     * set to Started and the method returns.
     * <p>
     * Asynchronous method -- Start synchronous transition on
     * another thread and return ASAP.
     */
    public final void start() {
        int state  = getState();
        int target = getTargetState();

        //  Has this state already been reached?
        if( state == Started ) {
            postStartEvent();
            return;
        }

        //  Set the target state
        if( target < Started ) {
            setTargetState(Started);
        }

        //  Start on a separate thread
        Thread thread = new Thread() {
            public void run() {
                if( AbstractPlayer.this.getState() < Started ) {
                    synchronousStart();
                }
            }
        };
        thread.setName("SynchronousStart Thread for " + this);

        getThreadQueue().addThread(thread);	// TODO: this seems to be able to cause an NPE if window is abruptly closed, using MDIApp sample
    }


    /**
     * Adds a Controller to be controlled by this Player.
     *
     * @param      newController
     *             The Controller to add
     *
     * @exception  NotRealizedError
     *             If this Player or the new Controller are not
     *             Realized.
     *
     * @exception  ClockStartedError
     *             If this Player or the new Controller are in the
     *             Started state.
     *
     * @exception  IncompatibleTimeBaseException
     *             Thrown by newController.setTimeBase() if the
     *             new Controller cannot use this player's
     *             TimeBase
     */
    public synchronized void addController(Controller newController)
        throws IncompatibleTimeBaseException
    {
        //  Return immediately if the new Controller
        //  is already being managed by this Player

        if( controllers.contains(newController) ||
            this == newController )
        {
            return;
        }

        int currentState = getState();

        //  Enforce state reqs for this Player
        if( currentState == Unrealized ||
            currentState == Realizing )
        {
            throw new NotRealizedError(
                "Cannot add Controller to an Unrealized Player");
        }

        //  Enforce state reqs for this Player
        if(currentState == Started) {
            throw new ClockStartedError(
                "Cannot add Controller to a Started Player");
        }

        int controllerState = newController.getState();

        //  Enforce state reqs for new controller
        if( controllerState == Unrealized ||
            controllerState == Realizing )
        {
            throw new NotRealizedError(
                "Cannot add Unrealized Controller to a Player");
        }

        //  Enforce state reqs for this Player
        if(controllerState == Started) {
            throw new ClockStartedError(
                "Cannot add Started Controller to a Player");
        }

        //  Set the time base for the new Controller.  This may
        //  throw an IncompatibleTimeBaseException.
        newController.setTimeBase(getTimeBase());

        //  Stop any forward-transitions of this Player and the
        //  to-be-managed Controller.  This will allow us to
        //  stabilize and synchronize the two.  Also update the
        //  current states of each.

        stop();
        newController.stop();
        currentState = getState();
        controllerState = newController.getState();

        //  According to the API, if the new Controller is not
        //  Prefetched, and this Player is Prefetched or
        //  Prefetching, then this Player must be transitioned
        //  back to the Realized state.

        if( controllerState < Prefetched &&
            currentState > Realized )
        {
            deallocate();
        }
        
        //  Set the media time for the new Controller
        newController.setMediaTime( getMediaTime() );

        //  Reset the stop time for the new Controller.  It will
        //  be stopped automatically when this Player's stop time
        //  is reached.
        newController.setStopTime(Clock.RESET);

        //  Set the rate for the new Controller.  If the new
        //  Controller cannot accomodate the current rate, then
        //  the rate for all Controllers and this Player will be
        //  set to 1.0.

        float rate = getRate();
        if( rate != newController.setRate(rate) ) {
            newController.setRate(1.0F);
            setRate(1.0F);
        }

        //  Add the controller to the list
        controllers.addElement(newController);

        //  Add ourselves as a listener to the new Controller so
        //  we can tell when the Controller has completed an event
        //  transition
        newController.addControllerListener(this);

        //  Update the overall duration
        updateDuration();
    }

    /**
     * Remove a Controller from the list of Controllers managed by
     * this Player.
     *
     * @param      oldController
     *             The Controller to remove
     */
    public synchronized void removeController(Controller oldController) {
        int currentState = getState();

        if(currentState == Unrealized ||
           currentState == Realizing)
        {
            throw new NotRealizedError(
                "Cannot remove Controller from an Unrealized Player");
        }

        if(currentState == Started) {
            throw new ClockStartedError(
                "Cannot remove Controller from a Started Player");
        }

        if( controllers.indexOf(oldController) == -1 ) {
            return;
        }

        //  Stop all transitions.  This is so that the
        //  doTransition methods don't need to synchronize on the
        //  controllers Vector.
        stop();

        //  This is already synchronized
        controllers.removeElement(oldController);

        //  Remove ourselves as a listener on this Controller
        oldController.removeControllerListener(this);

        //  Reset the timebase on the Controller
        try {
            oldController.setTimeBase(null);
        } catch(IncompatibleTimeBaseException e) {}

        //  Update the overall duration
        updateDuration();
    }

    /**
     * Gets the list of Controllers under control of this Player.
     */
    @SuppressWarnings("unchecked")
	protected Vector<Controller> getControllers() {
        return (Vector<Controller>)controllers.clone();
    }

    /**
     * Set the GainControl for this AbstractPlayer.  If the
     * AbstractPlayer does not support audio media, this method
     * should return null.
     *
     * @param      c
     *             The GainControl allowing control of the volume
     *             of this AbstractPlayer's media.
     */
    protected void setGainControl(GainControl c) {
        if( gainControl != null ) {
            removeControl(gainControl);
        }
        addControl(c);
        gainControl = c;
    }

    /**
     * Get the Gain Control for this Player.
     *
     * @return     The GainControl object, or null if it has
     *             not been set.
     */
    public GainControl getGainControl() {
        int currentState = getState();

        if(currentState == Unrealized ||
           currentState == Realizing)
        {
            throw new NotRealizedError(
                "Cannot get gain control on an Unrealized Player");
        }
        return gainControl;
    }

    /**
     * Set the visual Component for this AbstractPlayer.  If the
     * AbstractPlayer does not support video media, this method
     * should return null.
     *
     * @param      c
     *             A java.awt.Component on which the media is
     *             rendered.
     */
    protected void setVisualComponent(Component c) {
        visualComponent = c;
    }

    /**
     * Get the visual Component for this Player.
     *
     * @return     The visual Component, or null if it has
     *             not been set.
     */
    public Component getVisualComponent() {
        int currentState = getState();

        if(currentState == Unrealized ||
           currentState == Realizing)
        {
            throw new NotRealizedError(
                "Cannot get visual Component on an Unrealized Player");
        }
        return visualComponent;
    }

    /**
     * Set the control panal Component for this AbstractPlayer.
     *
     * @param      c
     *             A java.awt.Component providing control over
     *             this AbstractPlayer's media.
     */
    protected void setControlPanelComponent(Component c) {
        controlPanelComponent = c;
    }

    /**
     * Get the control panel Component for this Player.
     *
     * @return     The control panel Component, or null if
     *             it has not been set.
     */
    public Component getControlPanelComponent() {
        int currentState = getState();

        if(currentState == Unrealized ||
           currentState == Realizing)
        {
            throw new NotRealizedError(
                "Cannot get control panel Component on an Unrealized Player");
        }

        return controlPanelComponent;
    }

    ////////////////////////////////////////////////////////////
    //
    //  javax.media.MediaHandler methods
    //
    ////////////////////////////////////////////////////////////

    /**
     * Called by javax.media.Manager.  This is the litmus test
     * that tells whether this Player can support the given
     * DataSource.  If it can't, this method should throw a
     * IncompatibleSourceException.  Our only requirement here is
     * that the DataSource has not already been set.  Subclasses
     * may wish to override this method to extend the acceptance
     * criteria.
     *
     * @param      source
     *             The DataSource to test
     *
     * @exception  IncompatibleSourceException
     *             Thrown if the DataSource has already been set
     *             on this MediaHandler, or if the DataSource is
     *             not a PullDataSource
     */
    public void setSource(DataSource source)
        throws IncompatibleSourceException
    {
        //  Make sure the DataSource has only been set once
        if( this.source != null ) {
            throw new IncompatibleSourceException(
                "Datasource already set in MediaHandler " +
                    getClass().getName() );
        }

        this.source = source;
    }

    /**
     * Convenience method to get the DataSource for the Player.
     */
    public DataSource getSource() {
        return source;
    }

    ////////////////////////////////////////////////////////////
    //
    //  javax.media.ControllerListener methods
    //
    ////////////////////////////////////////////////////////////

    /**
     * Used to monitor events posted by this Player's managed
     * Controllers.  By keeping track of the actions of each
     * Controller, the Player can synchronize the state
     * transtitions of multiple Controllers.
     * <p>
     * This method is final because the controller-management
     * functionality of the AbstractPlayer will not work if it is
     * overridden.
     *
     * @param      e
     *             The ControllerEvent posted by one of the
     *             managed Controllers.
     */
    public final void controllerUpdate(ControllerEvent e) {
        synchronized(controllers) {
            if( e instanceof TransitionEvent ) {
                controllers.notifyAll();
            } else

            if( e instanceof ControllerErrorEvent ) {
                setControllerError((ControllerErrorEvent)e);
                controllers.notifyAll();
            }
        }
    }


    ////////////////////////////////////////////////////////////
    //
    //  ejmf.toolkit.media.content.AbstractController methods
    //
    ////////////////////////////////////////////////////////////

    /**
     *  Realize player and all of its managed Controllers.
     *  Subclasses should override doPlayerRealize() to do the
     *  actual work to transition the Controller.
     *  <p>
     *  This method should not be called directly.  Instead, call
     *  realize().
     */
    public final boolean doRealize() {

        try {
            //  Initialte the data transfer
            source.start();
        }
        
        catch(IOException e) {
            postEvent(
                new ResourceUnavailableEvent(this,
                    "Could not start DataSource") );
            return false;
        }

        //  No multi-Controller management is needed here because
        //  all Controllers are guaranteed to be Realized when
        //  they are added to this Player.  Furthermore, the
        //  Player itself must be realized before any Controllers
        //  are added.

        if(! doPlayerRealize() ) {
            return false;
        }

        //  Now that the Player is realized, getDuration() may
        //  be more accurate.
        updateDuration();

        return true;
    }

    /**
     *  Prefetch player and all of its managed Controllers.
     *  Subclasses should override doPlayerPrefetch() to do the
     *  actual work to transition the Controller.
     *  <p>
     *  This method should not be called directly.  Instead, call
     *  prefetch().
     */
    public final boolean doPrefetch() {

        //  Synchronize on the controllers Vector so that
        //  Controllers cannot be added or deleted while we are
        //  transitioning them.

        resetControllerError();

        //  Prefetch each managed Controller
        for(int i = 0; i < controllers.size(); i++) {
            Controller c = (Controller)controllers.elementAt(i);
            c.prefetch();
        }
        
        //  Prefetch this Player.  If it fails, then assume a
        //  ControllerErrorEvent was posted and return false.

        if(! doPlayerPrefetch() ) {
            return false;
        }

        //  Now wait for our all of our managed Controllers to
        //  complete the state transition or post an error.
        //  controllerUpdate() will catch such events and
        //  notify us.

        synchronized(controllers) {
            while(  controllerError == null &&
                    ! isStateReached(Prefetched) )
            {
                try { controllers.wait(); }
                catch(InterruptedException e) {}
            }
        }

        if( controllerError != null ) {
            postManagedControllerErrorEvent();
            return false;
        }

        //  Now that the Player is prefetched, getDuration() may
        //  be more accurate.
        updateDuration();

        return true;
    }

    /**
     *  Start player and all of its managed Controllers at the
     *  given time.  Subclasses should override doPlayerSyncStart()
     *  to do the actual work to transition the Controller.
     *  <p>
     *  This method should not be called directly.  Instead, call
     *  syncStart().
     */
    public final boolean doSyncStart(Time t) {

        //  Synchronize on the controllers Vector so that
        //  Controllers cannot be added or deleted while we are
        //  transitioning them.

        resetControllerError();

        //  SyncStart each managed Controller
        for(int i = 0; i < controllers.size(); i++) {
            Controller c = (Controller)controllers.elementAt(i);
            c.syncStart(t);
        }
        
        //  SyncStart this Player.  If it fails, then assume a
        //  ControllerErrorEvent was posted and return false.
        if( ! doPlayerSyncStart(t) ) {
            return false;
        }

        //  Now wait for our all of our managed Controllers to
        //  complete the state transition or post an error.
        //  controllerUpdate() will catch such events and
        //  notify us.

        synchronized(controllers) {
            while(  controllerError == null &&
                    ! isStateReached(Started) )
            {
                try { controllers.wait(); }
                catch(InterruptedException e) {}
            }
        }

        if( controllerError != null ) {
            postManagedControllerErrorEvent();
            return false;
        }

        return true;
    }

    /**
     *  Deallocate player on current thread.  Subclasses should
     *  override doPlayerDeallocate() to do the actual work to
     *  transition the Controller.
     *  <p>
     *  This method should not be called directly.  Instead, call
     *  deallocate().
     */
    public final boolean doDeallocate() {

        //  Synchronize on the controllers Vector so that
        //  Controllers cannot be added or deleted while we are
        //  transitioning them.

        resetControllerError();

        //  If there is a large number of managed Controllers,
        //  then deallocating each synchronously could be time
        //  consuming.  Instead, spawn a thread to deallocate
        //  each controller.  Later on, after this Player has
        //  been deallocated, wait for each deallocating
        //  thread to finish before returning.
        
        int size = controllers.size();
        Thread[] threads = new Thread[size];

        for(int i = 0; i < size; i++) {
            final Controller c =
                (Controller)controllers.elementAt(i);

            threads[i] = new Thread() {
                public void run() {
                    c.deallocate();
                }
            };

            threads[i].start();
        } 

        //  Deallocate this Player.  If it fails, then assume a
        //  ControllerErrorEvent was posted and return false.
        if( ! doPlayerDeallocate() ) {
            return false;
        }

        //  Wait for each Controller to deallocate
        for(int i = 0; i < size; i++) {
            try { threads[i].join(); }
            catch(InterruptedException e) {}
        }
            
        //  Check for errors during deallocation
        if( controllerError != null ) {
            postManagedControllerErrorEvent();
            return false;
        }

        return true;
    }

    /**
     *  Stop player on current thread.  Subclasses should override
     *  doPlayerStop() to do the actual work to transition the
     *  Controller.
     *  <p>
     *  This method should not be called directly.  Instead, call
     *  stop().
     */
    public final boolean doStop() {

        //  Synchronize on the controllers Vector so that
        //  Controllers cannot be added or deleted while we are
        //  transitioning them.

        resetControllerError();

        //  If there is a large number of managed Controllers,
        //  then stopping each synchronously could be time
        //  consuming, and could leave each Controller stopped
        //  at different media times.  Instead, spawn a thread
        //  to stop each controller.  Later on, after this
        //  Player has been stopped, wait for each thread to
        //  finish before returning.
        
        int size = controllers.size();
        Thread[] threads = new Thread[size];

        for(int i = 0; i < size; i++) {
            final Controller c =
                (Controller)controllers.elementAt(i);

            threads[i] = new Thread() {
                public void run() {
                    c.stop();
                }
            };

            threads[i].start();
        } 

        //  Stop this Player.  If it fails, then assume a
        //  ControllerErrorEvent was posted and return false.
        if( ! doPlayerStop() ) {
            return false;
        }

        //  Wait for each Controller to stop
        for(int i = 0; i < size; i++) {
            try { threads[i].join(); }
            catch(InterruptedException e) {}
        }
        
        //  Check for errors while stoping
        if( controllerError != null ) {
            postManagedControllerErrorEvent();
            return false;
        }

        return true;
    }

    /**
     * Close the Player.  First close all Controllers under the
     * control of this Player.  Then release resources held by
     * this Player.  Subclasses should implement doPlayerClose()
     * to add additional functionality.
     * <p>
     * This method should not be called directly.  Instead, call
     * close().
     */
    public synchronized final void doClose() {
        Vector<Controller> controllers = getControllers();

        //  Close all Controllers under the control of this Player
        for(int i = 0, n = controllers.size(); i < n; i++ ) {
            Controller c =
                controllers.elementAt(i);
            c.close();
        }

        try {
            //  Stop the data-transfer
            source.stop();

            //  Disconnect the DataSource
            source.disconnect();
        }
        catch(IOException e) {}

        //  Call implementation-specific functionality
        doPlayerClose();

        //  Release as many resources as we can
        controllers = null;
        source = null;
        gainControl = null;
        // duration = null;	// this causes problems getting the duration during, say, postDeallocateEvent, so let's leave it non-null
        controllerError = null;
    }

    /**
     * Sets the media time for this Player and all of its managed
     * Controllers.
     * <p>
     * This method should not be called directly.  Instead, call
     * setMediaTime().
     *
     * @param      t
     *             The media time to set.
     */
    public synchronized final void doSetMediaTime(Time t) {
        //  First set the media time on all of the managed Controllers
        for(int i = 0, n = controllers.size(); i < n; i++ ) {
            Controller c =
                (Controller)controllers.elementAt(i);

            c.setMediaTime(t);
        }
        
        //  Set the media time on this Player
        doPlayerSetMediaTime(t);
    }
    
    /**
     * Sets the rate for this Player and all of its managed
     * Controllers.  If any of the above cannot accomodate the
     * given rate, then the rate is set to 1.0 for all of the
     * above.
     * <p>
     * This method should not be called directly.  Instead, call
     * setRate().
     *
     * @param      rate
     *             The rate to set.
     */
    public synchronized final float doSetRate(float rate) {
        float actual;

        //  First set the rate on all of the managed Controllers
        for(int i = 0, n = controllers.size(); i < n; i++ ) {
            Controller c = (Controller)controllers.elementAt(i);
            actual = c.setRate(rate);

            if( rate != 1.0F && actual != rate ) {
                doSetRate(1.0F);
                return 1.0F;
            }
        }
        
        //  Set the rate on this Player
        actual = doPlayerSetRate(rate);

        if( ! controllers.isEmpty() && rate != 1.0F && actual != rate ) {
            doSetRate(1.0F);
            return 1.0F;
        }

        return actual;
    }
    
    ////////////////////////////////////////////////////////////
    //
    //  ejmf.toolkit.media.content.AbstractPlayer methods
    //
    ////////////////////////////////////////////////////////////

    /**
     * Sets the controller error that occurred while waiting for a
     * state transition.
     */
    private void setControllerError(ControllerErrorEvent e) {
        this.controllerError = e;
    }
    
    /**
     * Resets the ControllerErrorEvent.
     */
    private void resetControllerError() {
        setControllerError(null);
    }
    
    /**
     * Get the ControllerErrorEvent that occurred while waiting
     * for a state transition.
     */
//    private ControllerErrorEvent getControllerError() {
//        return controllerError;
//    }
    
    /**
     * Checks to see if the given Controller state has been
     * reached in a forward-transitioning Controller.
     *
     * @param      state
     *             The desired state.
     *
     * @return     true if the state has been reached, false
     *             otherwise.
     */
    private boolean isStateReached(int state) {
        synchronized(controllers) {
            for(int i = 0, n = controllers.size(); i < n; i++) {
                Controller controller = (Controller)controllers.elementAt(i);
                if( controller.getState() < state ) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * Checks to see if the all of this Player's managed
     * Controller have stopped.  Usually called from within
     * endOfMedia() to see if an EndOfMediaEvent can be posted.
     *
     * @return     true if all of this Player's managed
     *             Controller have stopped, false otherwise.
     */
    private boolean areControllersStopped() {
        synchronized(controllers) {
            for(int i = 0, n = controllers.size(); i < n; i++) {
                Controller controller = (Controller)controllers.elementAt(i);
                if( controller.getState() == Started ) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * Post a ManagedControllerErrorEvent to the Media Event
     * Queue.  Automatically fill in the managing Player, the
     * ControllerErrorEvent, and the message properties of the
     * ManagedControllerErrorEvent.
     */
    private void postManagedControllerErrorEvent() {
        String message =
            "Managing Player " + getClass().getName() +
            " received ControllerErrorEvent from " +
            controllerError.getSourceController().getClass().getName();

        postEvent( new ManagedControllerErrorEvent(
            this, controllerError, message ) );

        resetControllerError();
    }

    /**
     * Indicates to the framework that the end of media has been
     * reached.  Marks the media time, sets the current and target
     * states to Prefetched, and posts an EndOfMediaEvent.
     *
     * @exception  ClockStoppedException
     *             If the AbstractController is not in the Started
     *             state.
     */
    protected void endOfMedia()
        throws ClockStoppedException
    {
        synchronized(controllers) {
            //  Wait for all of the managed Controllers to stop or
            //  post an error.  controllerUpdate() will catch such
            //  events and notify us.

            //  controllerError was reset when doSyncStart() was called

            while( ! areControllersStopped() )
            {
                try { controllers.wait(); }
                catch(InterruptedException e) {}
            }
        }

        super.endOfMedia();
    }

    ////////////////////////////////////////////////////////////
    //
    //  Synchronous state-changing methods
    //
    //  These routines are called indirectly from the
    //  TransitionQueueMonitor class by way of a
    //  TransitionThread.  When these methods are called, the
    //  following assumptions can be made:
    //  
    //  1.  The current state is less than the desired state.  If
    //      doSyncStart() is called, the Player is guaranteed to
    //      be in the Prefetched state.
    //  
    //  2.  The target state is greater than or equal to the
    //      desired state.
    //  
    //  3.  Any state-related exceptions have been thrown
    //      already.
    //  
    //  4.  There are no other state-changing threads running.
    //
    ////////////////////////////////////////////////////////////

    /**
     * Start the AbstractPlayer ASAP.
     * <p>
     * Synchronous method -- return when transition complete
     */
    protected void synchronousStart() {
        //  Does the controller need to be prefetched?
        if( getState() < Prefetched ) {
            synchronousPrefetch();
        }

        //  Start ASAP
        synchronousSyncStart( getTimeBase().getTime() );
    }
}
